
package p2pgame;

import java.util.ArrayList;
import java.util.List;

import p2pgame.message.AbstractMessage;
import p2pgame.message.GameMessage;
import p2pgame.message.AckStartGame;
import p2pgame.message.AckJoin;
import p2pgame.message.Join;
import p2pgame.message.Move;
import p2pgame.message.NextTurn;
import p2pgame.message.Seed;
import p2pgame.message.SeedHash;
import p2pgame.message.StartGame;

import rice.environment.Environment;
import rice.p2p.commonapi.Application;
import rice.p2p.commonapi.Endpoint;
import rice.p2p.commonapi.Id;
import rice.p2p.commonapi.Message;
import rice.p2p.commonapi.Node;
import rice.p2p.commonapi.NodeHandle;
import rice.p2p.commonapi.RouteMessage;
import rice.pastry.commonapi.PastryIdFactory;

/**
 * Our Ludo application.
 * A peer who wants to join the network must create a LudoApp
 */
public class LudoApp implements Application {
	/**
	 * The Endpoint represents the underlieing node.  By making calls on the 
	 * Endpoint, it assures that the message will be delivered to a MyApp on whichever
	 * node the message is intended for.
	 */
	protected Endpoint endpoint;

	static int c=0;
	
	public List<Op> ops;
	public List<Player> players;

	List<Id> nextGame; // if initiator players for next game are gathered into this list
	
	public Id initiator; // The player who is responsible for initiating the next game

	Environment env;

	final int maxPlayers = 4;
	final int numOps = 5;

	Node node;
	
	// prematureMessages is a list of messages received a bit too early
	List<Pair<Id,Message>> prematureMessages = new ArrayList<Pair<Id,Message>>();

	public LudoApp(Node node,Environment env) {

		ops = new ArrayList<Op>();
		players = new ArrayList<Player>();

		nextGame = new ArrayList<Id>();

		this.env = env;
		this.node = node;

		// We are only going to use one instance of this application on each PastryNode
		this.endpoint = node.buildEndpoint(this, "ludo");

		// the rest of the initialization code could go here

		// now we can receive messages
		this.endpoint.register();
		System.out.println("LudoApp created --- ID: "+getPeerId().toStringFull());
	}

	public Id getPeerId(){
		return endpoint.getId();
	}

	public Node getNode(){
		return this.node;
	}

	private Id generateGameId(){
		List<byte[]> list = new ArrayList<byte[]>();
		for (Id id: nextGame){
			list.add(id.toByteArray());
		}
		byte[] result= Util.xor(list);
		return new PastryIdFactory(env).buildId(result);
	}


	public List<Id> generateOpIds(Id gameId){
		Util.log("GENERATING op ids from gameId "+gameId);
		byte[] ba=gameId.toByteArray();
		int lastIndex = ba.length-1;
		List<Id> opList = new ArrayList<Id>();
		for(int i=0;i<numOps;i++){
			// TODO: Allows maximum 256 ops ... whatever
			ba[lastIndex]=(byte)((int)ba[lastIndex]+1);
			opList.add(new PastryIdFactory(env).buildId(Util.hash(ba)));
		}
		Util.log("GENERATED op id list: "+opList);
		return opList;
	}

	// -------------------- Called by deliver() -----------------


	// Op
	private void startGame(StartGame m,Id to) throws RuntimeException{
		if (getOpByGameId(m.gameId,to)!=null){
			throw new RuntimeException("--- ERROR --- Peer has been selected as OP twice and our program can't handle that yet --- ERROR ---");
		}
		List<Id> opIds = generateOpIds(m.gameId);
		Op op = new Op(this,new Game(m.gameId,m.players,opIds),to);
		op.game.engine.start();
		ops.add(op);
		
		op.initDiceCalculation();
		
		// Clone list
		List<Pair<Id,Message>> temp = new ArrayList<Pair<Id,Message>>();
		for (Pair<Id,Message> p: prematureMessages){
			temp.add(p);	
		}
		for (Pair<Id,Message> p: temp){
			prematureMessages.remove(p);
			deliver2(p.getFirst(), (AbstractMessage)p.getSecond());
		}
		
	}

	// Players
	private void ackStartGame(AckStartGame m){
		initiator = null;
		if (getPlayerByGameId(m.gameId)==null){
			// Make sure that the turn of game initally kept by the player is behind the one
			// handled by nextTurn()
			Game cloneGame = new Game(m.ntMsg.getGame().gameId,m.ntMsg.getGame().players,generateOpIds(m.gameId));
			cloneGame.turn=-1;
			Player p = new Player(this,cloneGame);
			players.add(p);
		}
		getPlayerByGameId(m.gameId).nextTurn(m.ntMsg);
	}

	void send(Id id, AbstractMessage m){
		Util.log(this+" sends out "+m+" to "+id);
		endpoint.route(id, m, null);
	}
	void send(List<Id> list, AbstractMessage m){
		for (Id id: list){
			send(id, m);
		}
	}
	
	// Players
	private void join(Join m){
		// This player has no initiator
		if (this.initiator == null ){
			this.initiator = getPeerId();
			nextGame.add(this.initiator);
		}

		// This player is the initiator
		if (this.initiator.equals(getPeerId())){
			// This node is initiator
			send(m.from, new AckJoin(this.initiator));
			// Adds player to next game and starts new game if neccesary
			nextGame.add(m.from);
			if (nextGame.size()==maxPlayers){
				startGame();
			}
			// Someone else is this initiators
		} else {
			// Re-route join message to initiator			
			send(this.initiator, m);
		}
	}

	public boolean startGame() {
		
		// Has to be initiator to call this method
		if(this.initiator==null || !this.initiator.equals(getPeerId())) return false;
		
		List<Id> players = new ArrayList<Id>(nextGame);

		// Generate Op ids and send start-message
		Id gameId = generateGameId();
		Util.log("INITIATING GAME: " + gameId);
		List<Id> opIds = generateOpIds(gameId);
		
		AbstractMessage startMsg = new StartGame(getPeerId(),gameId,players);
		send(opIds,startMsg);
		
		nextGame.clear();
		initiator = null;
		return true;
	}

	// players
	private void ackJoin(AckJoin m){
		this.initiator=m.from;		
	}

	
	
	
	/**
	 * Called when we receive a message.
	 */
	public void deliver(Id to, Message message) {
		if (  message instanceof AbstractMessage){
			Util.log(this+" received "+message+" from "+((AbstractMessage)message).from+" to "+ to);
			


			deliver2(to,(AbstractMessage)message);
		}

	}

	public void deliver2(Id to, AbstractMessage message){
		if ( message instanceof GameMessage){
			GameMessage gmsg= (GameMessage)message;
			
			if ( message instanceof NextTurn){
				if (getPlayerByGameId(gmsg.gameId)==null) return;
				getPlayerByGameId(gmsg.gameId).nextTurn((NextTurn)message);
			}
			if (gmsg instanceof SeedHash){
				if (getOpByGameId(gmsg.gameId,to)== null){
					prematureMessages.add(new Pair<Id,Message>(to,gmsg));
				}else {
					getOpByGameId(gmsg.gameId,to).seedHash((SeedHash)gmsg);
				}
				
			}
			if (gmsg instanceof Seed){
				assert(getOpByGameId(gmsg.gameId,to)!=null);
				getOpByGameId(gmsg.gameId,to).seed((Seed)gmsg);
			}
			if ( gmsg instanceof Move){
				assert(getOpByGameId(gmsg.gameId,to)!=null);
				getOpByGameId(gmsg.gameId,to).move((Move)gmsg);	
			}
		}
		
		/* Messages received by ops */
		if ( message instanceof StartGame){
			startGame((StartGame)message,to);
		}

		/* Messages received by players */
		if ( message instanceof AckStartGame){
			ackStartGame((AckStartGame)message);
		}

		if ( message instanceof Join){
			join((Join)message);
		}

		if ( message instanceof AckJoin){
			ackJoin((AckJoin)message);
		}
	}
	

	private Player getPlayerByGameId(Id gameId){
		for(Player p: players){
			if (p.game.gameId.equals(gameId)){
				return p;
			}
		}
		return null;
	}

	private Op getOpByGameId(Id gameId,Id opId) {
		for(Op o: ops){
				if (o.game.gameId.equals(gameId) && o.opId.equals(opId)){
					return o;
				}
		
			
		}
		return null;
	}

	/**
	 * Called when you hear about a new neighbor.
	 * Don't worry about this method for now.
	 */
	public void update(NodeHandle handle, boolean joined) {
	}

	/**
	 * Called a message travels along your path.
	 * Don't worry about this method for now.
	 */
	public boolean forward(RouteMessage message) {
		return true;
	}

	public String toString() {
		return "LudoApp "+getPeerId();
	}
	
	// Interface ud ad til!
	
	public void joinGame(Id id){
		initiator=id;		
		send(id, new Join(getPeerId()));
	}

}
